package server;

import java.io.*;
import java.net.Socket;
import java.net.SocketException;
import java.nio.file.Files;
import java.util.InputMismatchException;

/**
 * @file Client_Handler.java
 *
 * @version 0.8
 * @author James Dibble 10009689
 *
 * Requires Java 7. Uses java.Files.readAllBytes()
 * 
 * Latest Source:
 * http://code.google.com/p/csp-server/source/browse/trunk/Server/src/server/Client_Handler.java
 */
public class Client_Handler implements Runnable {
    // <editor-fold defaultstate="collapsed" desc="Attributes">
    private Client_State state;
    private Socket sSocket;
    private PrintWriter pwClientOutput;
    private Server parent;
    public boolean bolDebug;
    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="Constructors">
    /**
     * Create a Client Handler that controls interactions via a Socket.
     *
     * @param sSocket
     * @param parent
     * @param bolDebug
     * @throws IOException
     */
    public Client_Handler(Socket sSocket, Server parent) throws IOException {
        this.state = Client_State.TERMS_OF_REFRENCE;
        this.sSocket = sSocket;
        this.pwClientOutput = new PrintWriter(sSocket.getOutputStream(), true);
        this.parent = parent;
    }
    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="Methods">
    @Override
    public void run() {
        try {
            BufferedReader brClientInput = new BufferedReader(new InputStreamReader(sSocket.getInputStream()));
            this.SendToClient(Client_Messages.TERMS_OF_SERVICE.toString());
            String strClientInput;
            
            //Loop until the socket is closed abnormally, the client disconnects
            //or the client has requested to not reciecve any more files or
            //did not accept the terms.
            while ((strClientInput = brClientInput.readLine()) != null
                    && sSocket.isConnected()
                    && state != Client_State.CLOSED) {
                
                //If verbose output is on display the contents of the last
                //stream from the client.
                if (Server.bolServerDebug) {
                    System.out.println(String.format("%s(%s): %s",
                            sSocket.getInetAddress().getHostName(),
                            sSocket.getInetAddress().toString(),
                            strClientInput));
                }
                
                //What state is the client in?
                if (state == Client_State.TERMS_OF_REFRENCE) {
                    if (strClientInput.equalsIgnoreCase("y")) {
                        this.SendToClient(parent.listResources());
                        state = Client_State.FILE_LIST;
                    } else if (strClientInput.equalsIgnoreCase("n")) {
                        sSocket.close();
                        state = Client_State.CLOSED;
                    } else {
                        this.SendToClient(Client_Messages.INVALID1.toString() 
                                + Client_Messages.TERMS_OF_SERVICE.toString());
                    }
                    
                } else if (state == Client_State.FILE_LIST) {
                    try {
                        int intTemp = Integer.parseInt(strClientInput);
                        Resource thisResource = null;
                        
                        //Try and find the resource the client requested
                        if ((thisResource = parent.getResourceByID(intTemp)) != null) {
                            if (this.parent.getServerType() == Server.Server_Type.DESCRIPTION_SERVER) {
                                thisResource.UploadResourceAsString();
                                this.SendToClient(thisResource.toString());
                                System.out.println(String.format("%s(%s): Downloaded %s",
                                        sSocket.getInetAddress().getHostName(),
                                        sSocket.getInetAddress().toString(),
                                        thisResource.getFileName()));
                            } else if (this.parent.getServerType() == Server.Server_Type.FILE_SERVER) {
                                thisResource.UploadResourceAsFile();
                                this.SendResourceToClient(thisResource);
                                System.out.println(String.format("%s(%s): Downloaded %s",
                                        sSocket.getInetAddress().getHostName(),
                                        sSocket.getInetAddress().toString(),
                                        thisResource.getFileName()));
                            }
                            this.SendToClient(Client_Messages.ANOTHER.toString());
                            this.state = Client_State.ANOTHER;
                            
                        } else { //The client has requested an invalid file.
                            this.SendToClient(Client_Messages.INVALID2.toString() + parent.listResources());
                        }
                    //The client has sent a non-numeric character.
                    } catch (InputMismatchException | NumberFormatException imEx) {
                        this.SendToClient(Client_Messages.INVALID2.toString() + parent.listResources());
                    }
                    
                } else if (state == Client_State.ANOTHER) {
                    if (strClientInput.equalsIgnoreCase("y")) {
                        this.SendToClient(parent.listResources());
                        state = Client_State.FILE_LIST;
                    } else if (strClientInput.equalsIgnoreCase("n")) {
                        sSocket.close();
                        state = Client_State.CLOSED;
                    } else {
                        this.SendToClient(Client_Messages.INVALID2.toString() + Client_Messages.ANOTHER.toString());
                    }
                }
            }
            System.out.println(String.format("%s(%s) has disconnected.",
                    sSocket.getInetAddress().getHostName(),
                    sSocket.getInetAddress().toString()));
        //Socket has closed abnormally.
        } catch (IOException ex) {
            if (bolDebug) {
                ex.printStackTrace(System.out);
            }
            System.out.println(String.format("%s(%s) has disconnected.",
                    sSocket.getInetAddress().getHostName(),
                    sSocket.getInetAddress().toString()));
        }
    }

    /**
     * Get the host name of the client. Using Windows on a network behind a
     * router implementing NAT, this will usually fail and return the IP of the
     * client.
     *
     * @return
     */
    public String getHostName() {
        return sSocket.getInetAddress().getHostName();
    }

    /**
     * Transmit a textual message to the client
     *
     * @param message
     */
    public void SendToClient(String message) {
        if (Server.bolServerDebug) {
            System.out.println(String.format("Server->%s(%s): %s",
                    sSocket.getInetAddress().getHostName(),
                    sSocket.getInetAddress().toString(),
                    message));
        }
        pwClientOutput.println(message);
        pwClientOutput.flush();
    }

    /**
     * Transmit a file to the client.
     *
     * @param thisResource
     * @throws SocketException
     * @throws FileNotFoundException
     * @throws IOException
     */
    public void SendResourceToClient(Resource thisResource) throws SocketException, FileNotFoundException, IOException {
        //Send to the client the size of the file to read and the name of the
        //file to create.
        this.SendToClient(Client_Messages.SENDING.toString() + (int) thisResource.getResourceAsFile().length());
        this.SendToClient(Client_Messages.SENDING.toString() + thisResource.getDownloadFileName());
        
        //Create a byte array the same size as the file.
        byte[] byteBuff = new byte[(int) thisResource.getResourceAsFile().length()];
        
        //Copy the contents of the file into the byte array.
        byteBuff = Files.readAllBytes(thisResource.getResourceAsFile().toPath());
        
        //Clear the sockets buffer and then send the file's contents.
        sSocket.getOutputStream().flush();
        sSocket.getOutputStream().write(byteBuff);
        sSocket.getOutputStream().flush();
        if (Server.bolServerDebug) {
            System.out.println(String.format("Server->%s(%s): %s",
                    sSocket.getInetAddress().getHostName(),
                    sSocket.getInetAddress().toString(),
                    "File uploaded."));
        }
    }

    /**
     * Close all connections to this client.
     * @throws IOException 
     */
    public void Disconnect() throws IOException {
        sSocket.close();
    }
    // </editor-fold>

    public enum Client_State {

        TERMS_OF_REFRENCE, FILE_LIST, ANOTHER, CLOSED
    }

    private enum Client_Messages {

        TERMS_OF_SERVICE {

            @Override
            public String toString() {
                return "Terms of reference. Do you accept? Y or N";
            }
        },
        ANOTHER {

            @Override
            public String toString() {
                return "Another? Y or N";
            }
        },
        INVALID1 {

            @Override
            public String toString() {
                return "Invalid input --- ";
            }
        },
        INVALID2 {

            @Override
            public String toString() {
                return "Invalid choice --- ";
            }
        },
        SENDING {

            @Override
            public String toString() {
                return "FILE_INCOMING:";
            }
        };
    }
}